// MyFamily - XML based genealogy program.
// Copyright (C) 2006  Doug Swisher
// See the file COPYING.txt for license details.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

using NUnit.Framework;


namespace DataModel
{
    public class FamilyPersona
    {
        // TODO - Doug - 9/4/2006 - is there a better place to put this enum?
        public enum SexEnum { Unknown, Male, Female };

        private FamilyExtract extract;
        private FamilyIndividual individual;
        private string id;
        private SexEnum sex;
        private PersonalName name;
        private FamilyPersona mother;
        private FamilyPersona father;
        private List<FamilyPersona> children = new List<FamilyPersona>();
        private List<FamilyEvent> events = new List<FamilyEvent>();
        private List<FamilyRelationship> relationships = new List<FamilyRelationship>();



        public FamilyPersona(FamilyExtract extract, string id)
        {
            this.extract = extract;
            this.id = id;
            this.sex = SexEnum.Unknown;
        }


        public void AddChild(FamilyPersona child)
        {
            this.children.Add(child);
        }


        public void RemoveChild(FamilyPersona child)
        {
            this.children.Remove(child);
        }


        public void AddEvent(FamilyEvent eve)
        {
            this.events.Add(eve);
        }


        public static FamilyPersona LoadFromGedCom(GedComNode node, GedComImportResults results, FamilyExtract extract)
        {
            // TODO - Doug - 8/29/2006 - we should strip off the leading and trailing @ signs, and consider adding a ged-specific prefix...
            string id = node.Xref;

            FamilyPersona persona = new FamilyPersona(extract, id);

            foreach (GedComNode child in node.Children)
            {
                switch (child.Tag)
                {
                    case "_EMAIL":
                        // TODO - Doug - 8/4/2006 - parse and store email
                        break;

                    case "AFN":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI AFN tags
                        break;

                    case "ADDR":
                        // TODO - Doug - 8/1/2006 - Figure out what to do with INDI ADDR tags
                        break;

                    case "ADOP":
                        // TODO - Doug - 8/12/2006 - Figure out what to do with INDI ADOP tags
                        break;

                    case "BAPM":
                    case "BIRT":
                    case "BURI":
                    case "CHR":
                    case "DEAT":
                    case "RESI":
                        FamilyEvent eve = FamilyEvent.LoadFromGedCom(child, results, extract);
                        persona.AddEvent(eve);
                        break;

                    case "CENS":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI CENS tags
                        break;

                    case "CHAN":
                        // TODO - Doug - 8/1/2006 - Figure out what to do with INDI CHAN tags
                        break;

                    case "EVEN":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI EVEN tags
                        break;

                    case "FAMC":
                    case "FAMS":
                        // Nothing to do here; we'll pick up family info from the FAM nodes
                        break;

                    case "NAME":
                        persona.name = PersonalName.Parse(child.Value);
                        break;

                    case "NOTE":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI NOTE tags
                        break;

                    case "OBJE":
                        // TODO - Doug - 8/12/2006 - Figure out what to do with INDI OBJE tags
                        break;

                    case "OCCU":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI OCCU tags
                        break;

                    case "PHON":
                        // TODO - Doug - 8/1/2006 - parse and store phone
                        break;

                    case "REFN":
                        // TODO - Doug - 8/1/2006 - parse and store reference number
                        break;

                    case "RIN":
                        // TODO - Doug - 8/12/2006 - Figure out what to do with INDI RIN tags
                        break;

                    case "SEX":
                        switch (child.Value)
                        {
                            case "F":
                                persona.sex = SexEnum.Female;
                                break;

                            case "M":
                                persona.sex = SexEnum.Male;
                                break;

                            default:
                                results.LogWarning("Unknown value '{0}' for SEX node.", child.Value);
                                break;
                        }
                        break;

                    case "SLGC":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI SLGC tags
                        break;

                    case "SOUR":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI SOUR tags
                        break;

                    case "SSN":
                        // TODO - Doug - 8/1/2006 - parse and store social security number
                        break;

                    case "TITL":
                        // TODO - Doug - 8/4/2006 - Figure out what to do with INDI TITL tags
                        break;

                    default:
                        results.LogUnhandledTag(node, child);
                        break;
                }
            }

            return persona;
        }


        public void SaveXml(XmlWriter writer)
        {
            writer.WriteStartElement("persona");
            writer.WriteAttributeString("id", this.Id);

            if (this.name != null)
            {
                // TODO - Doug - 8/3/2006 - Write out the given names and family names as separate bits
                writer.WriteElementString("name", this.name.ToString("/"));
            }

            switch (this.sex)
            {
                case SexEnum.Female:
                    writer.WriteElementString("sex", "F");
                    break;

                case SexEnum.Male:
                    writer.WriteElementString("sex", "M");
                    break;
            }

            if (this.father != null)
            {
                writer.WriteStartElement("father");
                writer.WriteAttributeString("id", this.Father.Id);
                writer.WriteValue(this.Father.Name.ToString());
                writer.WriteEndElement();
            }

            if (this.mother != null)
            {
                writer.WriteStartElement("mother");
                writer.WriteAttributeString("id", this.Mother.Id);
                writer.WriteValue(this.Mother.Name.ToString());
                writer.WriteEndElement();
            }

            foreach (FamilyEvent eve in this.events)
            {
                eve.SaveXml(writer);
            }

            // TODO - Doug - 8/3/2006 - write out all the other stuff...

            writer.WriteEndElement();
        }



        private FamilyPersona ParseRelatedPersonaXml(FamilyInfo info, XmlElement relatedElement)
        {
            string id = relatedElement.GetAttribute("id");

            FamilyPersona persona = null;

            if (info.Personas.ContainsKey(id))
            {
                persona = info.Personas[id];
            }
            else
            {
                // TODO - Doug - 8/29/2006 - should the extract be passed into this function??
                persona = new FamilyPersona(this.extract, id);
                info.Personas.Add(id, persona);
            }

            return persona;
        }



        public void LoadRelationship(FamilyInfo info, XmlElement relationshipElement, LoadResults results)
        {
            string typeString = relationshipElement.GetAttribute("type");
            if (string.IsNullOrEmpty(typeString))
            {
                results.LogWarning("Relationship element without type cannot be parsed.");
                return;
            }

            string otherPersonaId = relationshipElement.GetAttribute("personaId");
            if (string.IsNullOrEmpty(otherPersonaId))
            {
                results.LogWarning("Relationship element without other persona ID cannot be parsed.");
                return;
            }

            FamilyRelationship.Relationship type;
            try
            {
                type = (FamilyRelationship.Relationship) Enum.Parse(typeof(FamilyRelationship.Relationship), typeString, true);
            }
            catch (ArgumentException)
            {
                results.LogWarning("Relationship type '{0}' is not valid.", typeString);
                return;
            }

            // TODO - Doug - 9/7/2006 - handle the case where a persona is parsed before its relation...
            if (!info.Personas.ContainsKey(otherPersonaId))
            {
                results.LogWarning("Other persona ID '{0}' for relationship could not be located.", otherPersonaId);
                return;
            }

            FamilyPersona other = info.Personas[otherPersonaId];

            FamilyRelationship relationship = new FamilyRelationship(type, this, other);

            this.relationships.Add(relationship);
        }



        public void ParseXml(FamilyInfo info, XmlElement personaElement, LoadResults loadResults)
        {
            foreach (XmlElement elem in personaElement.SelectNodes("*"))
            {
                switch (elem.Name)
                {
                    case "name":
                        this.name = PersonalName.Parse(elem.InnerText);
                        break;

                    case "mother":
                        this.Mother = ParseRelatedPersonaXml(info, elem);
                        break;

                    case "father":
                        this.Father = ParseRelatedPersonaXml(info, elem);
                        break;

                    case "event":
                        FamilyEvent eve = new FamilyEvent(this.extract);
                        eve.ParseXml(elem);
                        this.events.Add(eve);
                        break;

                    case "sex":
                        switch (elem.InnerText.ToUpper())
                        {
                            case "M":
                                this.sex = SexEnum.Male;
                                break;

                            case "F":
                                this.sex = SexEnum.Female;
                                break;
                        }
                        break;

                    case "relationship":
                        this.LoadRelationship(info, elem, loadResults);
                        break;

                    default:
                        loadResults.LogWarning("Unknown persona element '{0}' found when parsing XML.", elem.Name);
                        break;
                }
            }
        }



        public string Id
        {
            get { return this.id; }
        }


        public PersonalName Name
        {
            get { return this.name; }
            set { this.name = value; }
        }


        public SexEnum Sex
        {
            get { return this.sex; }
        }


        public FamilyPersona Mother
        {
            get { return this.mother; }
            set
            {
                if (this.mother != null)
                {
                    this.mother.RemoveChild(this);
                }

                this.mother = value;

                this.mother.AddChild(this);
            }
        }


        public FamilyPersona Father
        {
            get { return this.father; }
            set
            {
                if (this.father != null)
                {
                    this.father.RemoveChild(this);
                }

                this.father = value;

                this.father.AddChild(this);
            }
        }


        public List<FamilyRelationship> Relationships
        {
            get { return this.relationships; }
        }


        public List<FamilyPersona> Children
        {
            get { return this.children; }
        }


        public List<FamilyEvent> Events
        {
            get { return this.events; }
        }


        public FamilyIndividual Individual
        {
            get { return this.individual; }
            set { this.individual = value; }
        }


        public FamilyExtract Extract
        {
            get { return this.extract; }
        }


        private static bool PersonasEqual(FamilyPersona left, FamilyPersona right)
        {
            if (left == null)
            {
                if (right == null)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                if (right == null)
                {
                    return false;
                }
                else
                {
                    return string.Equals(left.Id, right.Id);
                }
            }
        }



        private static bool IndividualsEqual(FamilyIndividual left, FamilyIndividual right)
        {
            if (left == null)
            {
                if (right == null)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                if (right == null)
                {
                    return false;
                }
                else
                {
                    return string.Equals(left.Id, right.Id);
                }
            }
        }



        public override bool Equals(object obj)
        {
            // Simple cases...
            if (obj == null)
            {
                return false;
            }

            FamilyPersona other = obj as FamilyPersona;

            if (other == null)
            {
                return false;
            }

            // Check members...
            if (!string.Equals(this.id, other.id))
            {
                return false;
            }

            if (!PersonalName.Equals(this.name, other.name))
            {
                return false;
            }

            if (this.sex != other.sex)
            {
                return false;
            }

            if (!IndividualsEqual(this.individual, other.individual))
            {
                return false;
            }

            if (!PersonasEqual(this.mother, other.mother))
            {
                return false;
            }

            if (!PersonasEqual(this.father, other.father))
            {
                return false;
            }

            if (this.children.Count != other.children.Count)
            {
                return false;
            }

            foreach (FamilyPersona thisChild in this.children)
            {
                bool found = false;
                foreach (FamilyPersona otherChild in other.children)
                {
                    if (thisChild.Id == otherChild.Id)
                    {
                        found = true;
                    }
                }
                if (!found)
                {
                    return false;
                }
            }

            if (this.events.Count != other.events.Count)
            {
                return false;
            }

            foreach (FamilyEvent thisEvent in this.events)
            {
                bool found = false;
                foreach (FamilyEvent otherEvent in other.events)
                {
                    if (thisEvent.Equals(otherEvent))
                    {
                        found = true;
                    }
                }
                if (!found)
                {
                    return false;
                }
            }

            return true;
        }


        public override int GetHashCode()
        {
            // TODO - Doug - 8/27/2006 - do we want to change this implementation at all?
            return base.GetHashCode();
        }
    }


    [TestFixture]
    public class PersonaEquality
    {
        private static FamilyExtract extract = new FamilyExtract("extract", new FamilySource("ExtractSource"));

        [Test]
        public void Nulls()
        {
            FamilyPersona p1 = new FamilyPersona(extract, "1");

            Assert.AreNotEqual(null, p1);
            Assert.AreNotEqual(p1, null);
        }


        [Test]
        public void SameIds()
        {
            FamilyPersona p1 = new FamilyPersona(extract, "1");
            FamilyPersona p2 = new FamilyPersona(extract, "1");

            Assert.AreEqual(p1, p2);
            Assert.AreEqual(p2, p1);
        }



        [Test]
        public void DifferentIds()
        {
            FamilyPersona p1 = new FamilyPersona(extract, "1");
            FamilyPersona p2 = new FamilyPersona(extract, "2");

            Assert.AreNotEqual(p1, p2);
            Assert.AreNotEqual(p2, p1);
        }


        [Test]
        public void SameNames()
        {
            FamilyPersona p1 = new FamilyPersona(extract, "1");
            FamilyPersona p2 = new FamilyPersona(extract, "1");

            p1.Name = PersonalName.Parse("Fred");
            p2.Name = PersonalName.Parse("Fred");

            Assert.AreEqual(p1, p2);
            Assert.AreEqual(p2, p1);
        }


        [Test]
        public void DifferentNames()
        {
            FamilyPersona p1 = new FamilyPersona(extract, "1");
            FamilyPersona p2 = new FamilyPersona(extract, "1");

            p1.Name = PersonalName.Parse("Fred");
            p2.Name = PersonalName.Parse("Barney");

            Assert.AreNotEqual(p1, p2);
            Assert.AreNotEqual(p2, p1);
        }


        // TODO - Doug - 8/27/2006 - add additional test cases for mother, father, events, etc.
    }
}
